////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (c) 2010 Gabriel Montagné Láscaris-Comneno and Alberto
//  Brealey-Guzmán.
//
//  Permission is hereby granted, free of charge, to any person obtaining a
//  copy of this software and associated documentation files (the "Software"),
//  to deal in the Software without restriction, including without limitation
//  the rights to use, copy, modify, merge, publish, distribute, sublicense,
//  and/or sell copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
//  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//  DEALINGS IN THE SOFTWARE.
//
////////////////////////////////////////////////////////////////////////////////

package com.rojored.controls
{

import com.rojored.utils.MathUtil;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.display.Loader;
import flash.display.Shader;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.TimerEvent;
import flash.net.URLRequest;
import flash.system.ApplicationDomain;
import flash.system.LoaderContext;
import flash.utils.Timer;
import flash.utils.getQualifiedClassName;
import mx.controls.Image;
import mx.core.UIComponent;
import mx.logging.ILogger;
import mx.logging.Log;
import mx.utils.ObjectUtil;
import org.bytearray.explorer.SWFExplorer;
import org.bytearray.explorer.events.SWFExplorerEvent;

//--------------------------------------
//  Events
//--------------------------------------

[Event(name="snap")]
[Event(name="ioError", type="flash.events.IOErrorEvent")]

/**
 *  loads and controls a turn-table assets swf---a swf that will have a
 *  sequence of images in its library.
 */
public class Turntable extends UIComponent
{

    //--------------------------------------------------------------------------
    //
    //  Class constants
    //
    //--------------------------------------------------------------------------

    /**
     *  Event type for when the turntable has finished animating and has
     *  finally "snapped" into position.
     */
    public static const SNAP:String = "snap";

    //--------------------------------------------------------------------------
    //
    //  Class variables
    //
    //--------------------------------------------------------------------------

    [Embed("crossfade.pbj", mimeType="application/octet-stream")]

    /**
     *  Crossfade bytecode
     */
    public static var CrossFadeByteCode:Class;

    /**
     *  Crossfade shader
     */
    private static var crossfadeShader:Shader = new Shader(new CrossFadeByteCode());

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor
     */
    public function Turntable()
    {
        super();
        blendMode = BlendMode.LAYER;
    }

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private var animationTimerClicked:Boolean;

    /**
     *  @private
     */
    private var logger:ILogger =
        Log.getLogger(getQualifiedClassName(this).replace(/\W+/g, "."));

    /**
     *  @private
     */
    private var lowerBitmap:Bitmap;

    /**
     *  @private
     */
    private var upperBitmap:Bitmap;

    /**
     *  @private
     */
    private var bitmaps:Vector.<BitmapData>;

    /**
     *  @private
     */
    private var currentFrame:Number = -1;

    /**
     *  @private
     */
    private var easingAnimationTimer:Timer;

    /**
     *  @private
     */
    private var maxBitmapWidth:int = 0;

    /**
     *  @private
     */
    private var maxBitmapHeight:int = 0;

    /**
     *  @private
     */
    private var posterFrame:Image;

    /**
     *  @private
     */
    private var startLoad:Boolean;

    /**
     *  @private
     */
    private var startUnload:Boolean;

    /**
     *  @private
     */
    private var swfExplorer:SWFExplorer;

    /**
     *  @private
     */
    private var turntableAssetLoader:Loader;

    /**
     *  @private
     */
    private var turntableAssetLoaded:Boolean;

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //--------------------------------------
    // autoLoad
    //--------------------------------------

    /**
     *  @private
     *  Storage for the autoLoad property
     */
    private var _autoLoad:Boolean;

    /**
     *  @private
     */
    private var autoLoadChanged:Boolean;

    [Bindable("autoLoadChanged")]

    /**
     *  if <code>true</code> the turntable asset will be loaded as soon as the
     *  source path is set.
     */
    public function get autoLoad():Boolean
    {
        return _autoLoad;
    }

    /**
     *  @private
     */
    public function set autoLoad(value:Boolean):void
    {
        if (_autoLoad == value)
            return;

        _autoLoad = value;
        autoLoadChanged = true;

        invalidateProperties();

        dispatchEvent(new Event("autoLoadChanged"));
    }

    //--------------------------------------
    //  animate
    //--------------------------------------

    /**
     *  if <code>true</code> intermediate frames will be displayed when
     *  flipping between images.
     */
    public var animate:Boolean = true;

    //--------------------------------------
    //  easingFactor
    //--------------------------------------

    /**
     *  softening factor for easing out the flip transitions
     */
    public var easingFactor:Number = 0.07;

    //--------------------------------------
    //  animationUpdate
    //--------------------------------------

    /**
     *  refresh update in milliseconds for transition animations
     */
    public var animationUpdate:int = 200;

    //--------------------------------------
    // bytesLoaded
    //--------------------------------------

    /**
     *  @private
     *  Storage for the bytesLoaded property
     */
    private var _bytesLoaded:Number = 0;

    [Bindable("bytesLoadedChanged")]

    /**
     *  bytes loaded for the turntable asset
     */
    public function get bytesLoaded():Number
    {
        return _bytesLoaded;
    }

    /**
     *  @private
     */
    private function setBytesLoaded(value:Number):void
    {
        if (_bytesLoaded == value)
            return;

        _bytesLoaded = value;
        dispatchEvent(new Event("bytesLoadedChanged"));
    }

    //--------------------------------------
    // bytesTotal
    //--------------------------------------

    /**
     *  @private
     *  Storage for the bytesTotal property
     */
    private var _bytesTotal:Number = 0;

    [Bindable("bytesTotalChanged")]

    /**
     *  total bytes for the asset
     */
    public function get bytesTotal():Number
    {
        return _bytesTotal;
    }

    /**
     *  @private
     */
    public function setBytesTotal(value:Number):void
    {
        if (_bytesTotal == value)
            return;

        _bytesTotal = value;
        dispatchEvent(new Event("bytesTotalChanged"));
    }

    //--------------------------------------
    // frameIndex
    //--------------------------------------

    /**
     *  @private
     *  Storage for the frameIndex property
     */
    private var _frameIndex:int;

    /**
     *  @private
     */
    private var frameIndexChanged:Boolean;

    [Bindable("frameIndexChanged")]

    /**
     *  the currently displayed frame index.
     */
    public function get frameIndex():int
    {
        return _frameIndex;
    }

    /**
     *  @private
     */
    public function set frameIndex(value:int):void
    {

        if (_frameIndex == value)
            return;

        _frameIndex = value;
        frameIndexChanged = true;

        invalidateProperties();
        dispatchEvent(new Event("frameIndexChanged"));
    }

    //--------------------------------------
    //  loaded
    //--------------------------------------

    [Bindable("loadedChanged")]

    /**
     *  if <code>true</code> the turntable bitmaps have been loaded and the
     *  component is ready to be used.
     */
    public function get loaded():Boolean
    {
        return bitmaps && bitmaps.length;
    }

    //--------------------------------------
    // loading
    //--------------------------------------

    /**
     *  @private
     *  Storage for the loading property
     */
    private var _loading:Boolean;

    /**
     *  @private
     */
    private var loadingChanged:Boolean;

    [Bindable("loadingChanged")]

    /**
     *  if <code>true</code> the turntable asset is currently loading
     */
    public function get loading():Boolean
    {
        return _loading;
    }

    /**
     *  @private
     */
    private function setLoading(value:Boolean):void
    {
        if (_loading == value)
            return;

        _loading = value;
        loadingChanged = true;
        dispatchEvent(new Event("loadingChanged"));
    }

    //--------------------------------------
    // maxFrameIndex
    //--------------------------------------

    [Bindable("maxFrameIndexChanged")]

    /**
     *  the maximum frame value
     */
    public function get maxFrameIndex():int
    {
        return bitmaps ? bitmaps.length - 1 : 0;
    }

    //--------------------------------------
    //  normalizeOnSnap
    //--------------------------------------

    /**
     *  if <code>true</code> after the transition has finished, the frame
     *  index will be normalized to the range in the bitmaps length range.
     */
    public var normalizeOnSnap:Boolean = true;

    //--------------------------------------
    // posterFrameSource
    //--------------------------------------

    /**
     *  @private
     *  Storage for the posterFrameSource property
     */
    private var _posterFrameSource:String;

    /**
     *  @private
     */
    private var posterFrameSourceChanged:Boolean;

    [Bindable("posterFrameSourceChanged")]

    /**
     *  the path to the posterframe image file
     */
    public function get posterFrameSource():String
    {
        return _posterFrameSource;
    }

    /**
     *  @private
     */
    public function set posterFrameSource(value:String):void
    {
        if (_posterFrameSource == value)
            return;

        _posterFrameSource = value;
        posterFrameSourceChanged = true;

        invalidateProperties();

        dispatchEvent(new Event("posterFrameSourceChanged"));
    }

    //--------------------------------------
    //  posterFrameIndex
    //--------------------------------------

    /**
     *  Which of the bitmap indexes matches the poster frame.
     */
    public var posterFrameIndex:int = 0;

    //--------------------------------------
    //  snapThreshold
    //--------------------------------------

    /**
     *  Animation snapThreshold
     */
    public var snapThreshold:Number = 0.005;

    //--------------------------------------
    // source
    //--------------------------------------

    /**
     *  @private
     *  Storage for the source property
     */
    private var _source:String;

    /**
     *  @private
     */
    private var sourceChanged:Boolean;

    [Bindable("sourceChanged")]

    /**
     *  the URL to the turntable asset swf
     */
    public function get source():String
    {
        return _source;
    }

    /**
     *  @private
     */
    public function set source(value:String):void
    {
        if (_source == value)
            return;

        _source = value;
        sourceChanged = true;
        invalidateProperties();
        dispatchEvent(new Event("sourceChanged"));

    }

    //--------------------------------------------------------------------------
    //
    //  Overriden methods: UIComponent
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function createChildren():void
    {
        super.createChildren();
        if (!lowerBitmap)
        {
            lowerBitmap = new Bitmap();
            addChild(lowerBitmap);
        }

        if (!upperBitmap)
        {
           upperBitmap = new Bitmap();
           upperBitmap.blendMode = BlendMode.SHADER;
           upperBitmap.blendShader = crossfadeShader;
           addChild(upperBitmap);
        }

        if (!posterFrame)
        {
           posterFrame = new Image();
           addChild(posterFrame);
        }
    }

    /**
     *  @private
     */
    override protected function commitProperties():void
    {
        super.commitProperties();

        if (posterFrameSourceChanged)
        {
            posterFrameSourceChanged = false;
            posterFrame.source = posterFrameSource;
            posterFrame.visible = !loaded;

        }

        if (sourceChanged && loaded)
            unloadAsset(true);

        if (autoLoadChanged || sourceChanged || startLoad)
        {
            autoLoadChanged = false;
            sourceChanged = false;

            if (source && !loaded && !loading && (autoLoad || startLoad))
                loadTurntableAsset(source);

            startLoad = false;
        }

        if (startUnload)
        {
            startUnload = false;
            snap();
            if (loaded)
                unloadAsset();
        }

        if (turntableAssetLoaded)
        {

            turntableAssetLoaded = false;
            setLoading(false);

            var definitions:Array = swfExplorer.getDefinitions().sort();
            logger.debug("get definitions: " + definitions.join("::").substr(0, 100));
            var assetsApplicationDomain:ApplicationDomain =
                turntableAssetLoader.contentLoaderInfo.applicationDomain;

            var className:String;
            var assetClass:Class;
            var assetData:BitmapData;

            bitmaps = new Vector.<BitmapData>();
            while (definitions.length)
            {
                className = definitions.shift();
                var asset:*;
                try
                {
                    asset = new (assetsApplicationDomain.getDefinition(className))();
                }
                catch (error:Error)
                {
                    logger.debug("error instantiating class: " + error.message);
                    asset = new (assetsApplicationDomain.getDefinition(className))(1, 1);
                }

                assetData =
                        asset is Bitmap ? Bitmap(asset).bitmapData
                    :   asset is BitmapData ? BitmapData(asset)
                    :   null
                    ;

                if (!assetData)
                    continue;

                bitmaps.push(assetData);
                maxBitmapWidth =
                    Math.max(assetData.width, maxBitmapWidth);

                maxBitmapHeight =
                    Math.max(assetData.height, maxBitmapHeight);

            }
            dispatchEvent(new Event("maxFrameIndexChanged"));
            dispatchEvent(new Event("loadedChanged"));
            flipToFrame(posterFrameIndex);
            posterFrame.visible = false;
        }

        if (frameIndexChanged)
        {
            frameIndexChanged = false;
            if (animate)
            {
                startAnimation();
            }
            else
                flipToFrame(frameIndex);
        }

    }

    /**
     *  @private
     */
    override protected function updateDisplayList(unscaledWidth:Number,
                                                  unscaledHeight:Number):void
    {
        super.updateDisplayList(unscaledWidth, unscaledHeight);
        posterFrame.setActualSize(
            posterFrame.getExplicitOrMeasuredWidth(),
            posterFrame.getExplicitOrMeasuredHeight()
        );
    }

    /**
     *  @private
     */
    override protected function measure():void
    {
        super.measure();
        measuredWidth = Math.max(
            maxBitmapWidth,
            posterFrame.getExplicitOrMeasuredWidth()
        );

        measuredHeight = Math.max(
            maxBitmapHeight,
            posterFrame.getExplicitOrMeasuredHeight()
        );

    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  starts loading the turntable asset.
     */
    public function load():void
    {
        startLoad = true;
        invalidateProperties();
    }

    /**
     *  unloads a turntable asset
     */
    public function unload():void
    {
        startUnload = true;
        invalidateProperties();
    }

    /**
     *  @private
     */
    private function loadTurntableAsset(source:String):void
    {
        if (!swfExplorer)
        {
            swfExplorer = new SWFExplorer();
            swfExplorer.addEventListener(
                SWFExplorerEvent.COMPLETE,
                swfExplorer_completeHandler
            );
            swfExplorer.addEventListener(
                IOErrorEvent.IO_ERROR,
                swfExplorer_ioErrorHandler
            );
            swfExplorer.addEventListener(
                IOErrorEvent.IO_ERROR,
                swfExplorer_ioErrorHandler
            );
            swfExplorer.addEventListener(
                ProgressEvent.PROGRESS,
                swfExplorer_progressHandler
            );
        }
        swfExplorer.load(new URLRequest(source));
        setLoading(true);
    }

    /**
     *  @private
     */
    private function unloadAsset(leaveBitmaps:Boolean = false):void
    {
        bitmaps = null;
        dispatchEvent(new Event("maxFrameIndexChanged"));
        dispatchEvent(new Event("loadedChanged"));
        posterFrame.visible = true;
        setBytesTotal(0);
        setBytesLoaded(0);
        if (leaveBitmaps)
            return;
        lowerBitmap.bitmapData = null;
        upperBitmap.bitmapData = null;
    }

    /**
     *  @private
     */
    private function snap():void
    {
        flipToFrame(frameIndex);
        stopAnimation();
    }

    /**
     *  @private
     */
    private function flipToFrame(frame:Number):void
    {
        if (!bitmaps)
            return;

        if (!bitmaps.length)
            return;

        currentFrame = frame;

        var lowerIndex:int = Math.floor(currentFrame);
        var upperIndex:int = Math.ceil(currentFrame);

        lowerBitmap.bitmapData =
            bitmaps[MathUtil.modulo(lowerIndex, bitmaps.length)];

        upperBitmap.bitmapData =
            bitmaps[MathUtil.modulo(upperIndex, bitmaps.length)];

        crossfadeShader.data.intensity.value = [currentFrame - lowerIndex];
        upperBitmap.blendShader = crossfadeShader;

    }

    /**
     *  @private
     */
    private function startAnimation():void
    {
        addEventListener(Event.ENTER_FRAME, enterFrameHandler);

    }

    /**
     *  @private
     */
    private function stopAnimation():void
    {
        removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    //--------------------------------------------------------------------------
    //
    //  Event handlers
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private function enterFrameHandler(event:Event):void
    {

        var delta:Number = (frameIndex - currentFrame) * easingFactor;
        if (Math.abs(delta) < snapThreshold)
        {
            snap();
            if (normalizeOnSnap)
            {
                _frameIndex = MathUtil.modulo(frameIndex, bitmaps.length);
                flipToFrame(_frameIndex)
                dispatchEvent(new Event("frameIndexChanged"));
            }

            dispatchEvent(new Event(SNAP));
        }
        else
        {
            flipToFrame(currentFrame + delta);
        }
    }

    /**
     *  @private
     */
    private function turntableAssetLoader_initHandler(event:Event):void
    {
        logger.debug("turntableAssetLoader_initHandler: " + event);
        turntableAssetLoaded = true;
        invalidateProperties();
        invalidateSize();
    }

    /**
     *  @private
     */
    private function swfExplorer_completeHandler(event:SWFExplorerEvent):void
    {
        logger.debug("swfExplorer_completeHandler: " + event);
        if (!turntableAssetLoader)
        {
            turntableAssetLoader = new Loader();
            turntableAssetLoader.contentLoaderInfo.addEventListener(
                Event.INIT,
                turntableAssetLoader_initHandler
            );
        }
        var loaderContext:LoaderContext = new LoaderContext();

        // FIXME: this must be turned on for AIR, but not for Flex
        // loaderContext.allowLoadBytesCodeExecution = true;
        turntableAssetLoader.loadBytes(swfExplorer.data, loaderContext);
    }

    /**
     *  @private
     */
    private function swfExplorer_progressHandler(event:ProgressEvent):void
    {
        setBytesTotal(event.bytesTotal);
        setBytesLoaded(event.bytesLoaded);
    }

    /**
     *  @private
     */
    private function swfExplorer_ioErrorHandler(event:IOErrorEvent):void
    {
        setLoading(false);
        dispatchEvent(event);
    }
}
}
